package peer

import (
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"hundsun.com/hsl/hschain/common/util"

	"hundsun.com/hsl/hschain/common/types"
	"hundsun.com/hsl/hschain/common/version"
	"hundsun.com/hsl/hschain/p2p/common/constant"
	p2putils "hundsun.com/hsl/hschain/p2p/common/utils"
	pbledger "hundsun.com/hsl/hschain/protos/ledger"
	"hundsun.com/hsl/hschain/store/mq"

	"github.com/libp2p/go-libp2p-core/network"
	"github.com/libp2p/go-libp2p-core/peer"
	"github.com/multiformats/go-multiaddr"
	pbp2p "hundsun.com/hsl/hschain/protos/p2p"
)

const diffHeightValue = 512

func (p *Protocol) getLocalPeerInfo() *pbp2p.PeerInfo {
	var peerInf pbp2p.PeerInfo
	peerInf.Name = p2putils.PeerIDEncode(p.P2pNode.Host.ID())
	ip, port := parseIPAndPort(p.getExternalAddr())
	peerInf.Addr = ip
	peerInf.Port = int32(port)
	peerInf.Version = version.GetVersion()

	msgBlock := p.WorkClient.NewMessage(types.BlockChainModule, mq.ModeWorker.String(), types.EventGetLastBlock, nil)
	if err := p.WorkClient.Send(msgBlock, true); err != nil {
		p.Log.Errorf("latest block from blockchain request failed reason:%v", err)
		return &peerInf
	}
	resp, err := p.WorkClient.WaitTimeout(msgBlock, time.Second*3)
	if err != nil {
		p.Log.Errorf("latest block from blockchain response failed reason:%v", err)
		return &peerInf
	}

	if resp.Data != nil {
		blk := resp.Data.(*pbledger.ExecutedBlock)
		if blk != nil && blk.Block != nil {
			peerInf.Header = blk.Block.Header
		}
	}
	return &peerInf
}

func (p *Protocol) refreshLocalPeerInfo() {
	p.Log.Debugf("refresh local peerInfo")
	selfPeer := p.getLocalPeerInfo()
	if selfPeer != nil {
		if selfPeer.Header != nil {
			p.Log.Debugf("refresh local peerInfo height:%d", selfPeer.Header.Number)
		}
		selfPeer.Self = true
		p.P2pNode.PeerInfoMgr.Refresh(selfPeer)
	}
}

func (p *Protocol) refreshPeerInfo() {
	p.Log.Debugf("refresh remote peerInfo")
	if !atomic.CompareAndSwapInt32(&p.refreshing, 0, 1) {
		return
	}
	defer atomic.StoreInt32(&p.refreshing, 0)
	var wg sync.WaitGroup
	for _, remoteID := range p.P2pNode.ConnMgr.FetchConnPeers(func(id peer.ID) bool {
		return !p.P2pNode.Manager.Contains(id)
	}) {
		if p.checkDone() {
			p.Log.Warnf("refreshPeerInfo process done")
			return
		}
		if remoteID == p.P2pNode.Host.ID() {
			continue
		}

		wg.Add(1)
		go func(pid peer.ID) {
			defer wg.Done()
			pInfo, err := p.queryPeerInfo(pid)
			if err != nil {
				p.Log.Errorf("refreshPeerInfo error %s, pid %s", err.Error(), pid)
				return
			}
			if pInfo == nil {
				return
			}
			p.Log.Debugf("refresh peer info, pid:%v", pid)
			p.P2pNode.PeerInfoMgr.Refresh(pInfo)
		}(remoteID)
	}
	wg.Wait()
}

func (p *Protocol) checkOutBound(height uint64) {
	for _, pinfo := range p.P2pNode.PeerInfoMgr.FetchAll() {
		if pinfo.GetHeader().GetNumber()+diffHeightValue < height {
			pid, err := peer.Decode(pinfo.GetName())
			if err != nil {
				continue
			}
			// 断开向外的主动连接
			for _, conn := range p.P2pNode.Host.Network().ConnsToPeer(pid) {
				//判断是Inbound 还是Outbound
				if conn.Stat().Direction == network.DirOutbound {
					// 拉入连接黑名单
					_ = conn.Close()
				}
			}
		}
	}
}

func (p *Protocol) queryPeerInfo(pid peer.ID) (*pbp2p.PeerInfo, error) {
	resp, err := p.P2pNode.SyncMsg(&pbp2p.P2PMsg{}, pid, constant.PeerInfoAddress)
	if err != nil || resp == nil {
		return nil, err
	}
	var peerInfo = &pbp2p.PeerInfo{}
	_, err = util.Bytes2Struct(peerInfo, resp.Payload)
	if err != nil {
		return nil, err
	}
	return peerInfo, nil
}

func (p *Protocol) setExternalAddr(addr string) {
	ip, _ := parseIPAndPort(addr)
	if isPublicIP(ip) {
		p.mutex.Lock()
		p.externalAddr = addr
		p.mutex.Unlock()
		ma, _ := multiaddr.NewMultiaddr(addr)
		p.P2pNode.Host.Peerstore().AddAddr(p.P2pNode.Host.ID(), ma, time.Hour*24)
	}
}

func (p *Protocol) getExternalAddr() string {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	return p.externalAddr
}

func (p *Protocol) getPublicIP() string {
	ip, _ := parseIPAndPort(p.getExternalAddr())
	return ip
}

func (p *Protocol) containsPublicIP(pid peer.ID) bool {
	for _, maddr := range p.P2pNode.Host.Peerstore().Addrs(pid) {
		if ip, _ := parseIPAndPort(maddr.String()); isPublicIP(ip) {
			return true
		}
	}
	return false
}

func parseIPAndPort(multiAddr string) (ip string, port int) {
	split := strings.Split(multiAddr, "/")
	if len(split) < 5 {
		return
	}
	port, err := strconv.Atoi(split[4])
	if err != nil {
		return
	}
	ip = split[2]
	return
}
